Repository navigation
feat(cli): cloudhal deploy --release installs the signed release (#227) - #238
Merged
Merged
Conversation
RFC-0046 §11b with the §9 amendments: instead of building, install this CLI version's signed release. - The signed key and yank lists, from cloudhal.dev on every run or from --from, verified with the pinned root and checked against the CLI's yank memory (~/.cloudhal/release-trust.json); no fallback, no override. Before the root ceremony no root is pinned: refused, naming the ceremony. - The release: published (not a draft) with a bundle, v0.9.0 or newer; verifyRelease with the keys reduced by both lists and yanks applied; the build attestation (--allow-single-factor when gh is unavailable). - Before the tarball: the theme check against the CLI's starter (PostCSS up to Vite's workspace root; --discard-local-theme), the cloudhal.json build keys (config-build-keys.ts, a table mapped over ProjectConfig), and the bundle's minimum wrangler. Installations, multi-site and Workers Builds are refused. - Path-safe extraction into a fresh .cloudhal/deploy/release/<v>/ and no_bundle wrangler configs that upload exactly those bytes; then the usual provisioning, secrets, downgrade guard (manifest level), deploy and push. - Refactors: siteConfigFor, shipWorkers' site config, deploy/finish.ts, downgradeGuard's options object, OWN_KEYS exported. - Tests use release-verify's synthetic signed bundle and no network. Closes #227 Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Madraka <[email protected]>
3 tasks
- release-verify typechecks under every consumer's lib settings (create-cloudhal: @types/node only): ArrayBuffer-backed WebCrypto inputs, the gzip pipe typed through, the reader's result type inferred. - deploy --release refuses a version argument (npx @cloudhal/cli@<v>). - --discard-local-theme lists every file not deployed. - Steps 1-6 (trust, lookup, checks, verify, attestation, extract) run before loadDeploy: no prompt, cloudhal.json write or store open before then. - The yank memory is re-read and merged monotonically before the rename; a failed save is trust-state, naming the file. - An attestation that could not finish (timeout, signal, rate limit, network) says so; still a refusal. - GH_TOKEN/GITHUB_TOKEN for the API lookup only; 403/429 is github-rate-limited. - An existing skills/ (even empty) is a theme difference. - Docs: every check and refusal in installation.md; a README entry. - Drift guard: astro-renderer's singleSiteId, CLOUDHAL_SITE_ID over the built id (test). The dataDir guard is #239. - Tests: URL checks compare hostnames (CodeQL). Refs #227 Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Madraka <[email protected]>
An expired or revoked GH_TOKEN/GITHUB_TOKEN gets 401 even on public endpoints, which fell through to release-unavailable. On a 401 the lookup now retries once without the header; when that succeeds it goes on with a one-line warning, otherwise the retry's own status decides. Refs #227 Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Madraka <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
cloudhal deploy --release(#227 part 2), as RFC-0046 §11b specifies, with the §9 amendments from #226. It installs the signed release of the CLI's own version instead of building the project.The code is in
packages/cli/src/commands/deploy/release/, plus the key table inpackages/cli/src/config-build-keys.ts.Order (nothing written outside
.cloudhal/deploy/, and nothing uploaded, before the attestation passes)project-unsupported,release-unavailable):trust.ts).keys.jsonandyanked.json, each with its.sig, are fetched on every run fromTRUST_BASE_URL, or read from--from <dir>. Any failure istrust-unavailable, with no fallback to a release asset or a cache.PINNED_ROOTSand must be unexpired.YankMemorylives in~/.cloudhal/release-trust.json, written atomically with mode 0600. It is checked byverifyKeysandverifyYanked, and is stored only after both pass. A damaged memory file is refused, never dropped.PINNED_ROOTSis empty, so the command refuses with a message naming the owner's root ceremony (docs/security/release-signing-keys.md › First-time setup). That is tested, and nothing is fetched.fetch-release.ts).GET api.github.com/repos/cloudhal-dev/cloudhal/releases/tags/v<v>:draft: true, or a missing bundle asset isrelease-unavailable, and the message says to deploy without--release;.cloudhal/deploy/release-download/<v>/, with byte caps fromLIMITS, streamed to disk.theme-check.ts):src/,public/andskills/, plusastro.config.mjs,tsconfig.jsonandjsconfig.json, with this CLI's own starter (assetDir("starters")/blogthroughstarterFilter, ascloudhal initcopies it);postcsskey in apackage.json, from the project up to Vite's workspace root;.DS_Store,Thumbs.dbanddesktop.ini; the refusal lists the first 20 differences;--discard-local-themelists the files that are not deployed and changes nothing on disk.config-build-keys.ts):ProjectConfigkey, plusmedia, has a class:deploy,runtime,buildorrefused;analytics,commentsandmediaare classed per sub-key;ProjectConfig, sotscfails on an unclassed key;comments.availableother thantrue, ormedia.transformother thannone(unless the manifest'sfeatures.mediaTransformVaris true), is refused, and so is an unknown key or sub-key;--discard-local-themedoes not cover these.manifest.toolchain.wrangler(wrangler-too-oldnames the required version).verifyRelease:keys = mergeTrust(PINNED_KEYS, { keys, yanked })andyanked, so a revoked key giveskey-revokedand a yanked version givesyanked.planVersionis 1.@cloudhal/release-verify/attestation):failed→attestation-failed;unavailable→attestation-unavailable, with how to install gh 2.68+ and rungh auth login;--allow-single-factorcontinues on the release key alone and prints a line saying so. The flag is never implied.extract.ts) into.cloudhal/deploy/release/<v>/{site,admin}/{modules,assets}:path.resolved and must stay inside;wx, so a duplicate fails;_headerscomes from the manifest.configs.ts):siteWorkerConfigandadminWorkerConfigas today, plusmain(the extracted main module),base_dir,no_bundle,find_additional_modules,preserve_file_names, and onerulesentry per module type (exact globs,fallthrough: false);assetsis the extracted directory with anASSETSbinding plus the manifest's routing config;.cloudhal/deploy/release-config/{site,admin}/wrangler.jsonc.-cwith the release configs), state, the first push and the purge;manifest.levels.schedules;--release --dry-runstops before wrangler.Refactors (no behaviour change for
cloudhal deploy)buildSiteis split:siteConfigForwrites the config, andbuildSitestill builds.shipWorkerstakes an optional site config.deploy()moved todeploy/finish.ts.downgradeGuardtakes{ adminUrl, token, localLevel? }.OWN_KEYSis exported.CLI and docs
--release,--from(with--release),--allow-single-factorand--discard-local-theme. The last three are refused without--release.help.tsis updated.docs/start/installation.mdgains Installing the signed release.@cloudhal/climinor.Review round 1
CI: typecheck in every consumer.
@cloudhal/release-verifynow typechecks under@types/nodealone, which is whatcreate-cloudhaluses, as well as under the Workers types:ArrayBuffer-backed views, copied only when a buffer is shared;DecompressionStreamis typed through explicitly;turbo run typecheckpasses for every package.CodeQL. The test's URL checks parse the URL and compare
hostnameexactly.srchas no substring URL checks.No version argument.
deploy --release 0.9.0is refused, and the message points tonpx @cloudhal/cli@<version> deploy --release.--discard-local-themelists every file that is not deployed. The 20-file truncation stays for the refusal.Order of operations. The trust lists, the lookup, the checks, verification, the attestation and extraction all run before
loadDeploy. So no analytics prompt,cloudhal.jsonwrite or store open happens until the release has verified. There is a test for this.Yank memory. The file is re-read just before the atomic rename and merged monotonically (
mergeMemory):keysIssuedAt, and the union of versions, revoked keys and retired yank keys;A failure to save is its own code,
trust-state, and names the file and the cause.Attestation. A timeout, a signal, a rate limit or a network error is worded as "could not be checked … still a refusal". Only a real verification failure says the files were not built by
bundle-build.yml.GitHub API.
GH_TOKEN, elseGITHUB_TOKEN, goes to api.github.com only, and a test checks that no other request carries it. A 403 or 429 is its own code,github-rate-limited, and namesGH_TOKEN.skills/. An existingskills/is refused even when empty, and a file namedskillsis refused too.Docs.
docs/start/installation.mdlists every check and refusal, in order.packages/cli/README.mdhas a--releaseentry.Drift guards.
CLOUDHAL_SITE_IDoverride is added. The choice is nowsingleSiteIdinastro-renderer/src/site-dispatch.ts, which the cloudflare entry uses, andtest/site-dispatch.test.tschecks that the var wins over the built id.dataDir" is not small. The integration putsdataDirinto the virtual config on every target, so the fix is to stop that for cloudflare and prove it on the workerd suites. That is follow-up release: drift guard that the cloudflare target ignores dataDir (RFC-0046 §11b) #239.@cloudhal/release-verify package and cloudhal deploy --release (RFC-0046) #227's done-when needs
@cloudhal/release-verifypublished, which it is not yet, so this PR now says Refs @cloudhal/release-verify package and cloudhal deploy --release (RFC-0046) #227 instead of closing it.Changeset. Now also
@cloudhal/release-verifypatch and@cloudhal/astro-rendererpatch.@cloudhal/clinow depends on@cloudhal/release-verify(workspace:*). That package is not on npm yet: the owner's placeholder step, then trusted publishing. So the next release must publishrelease-verifyalong with the CLI. If publish-ci still leaves it out, the published CLI's install would fail.Tests (no network: every URL is served by a recording fake; run one file at a time)
deploy-release.test.ts(15). It uses the synthetic signed bundle from release-verify's tests and a throwaway root and yank key signed withscripts/release-keys.ts. It covers:key-revoked) and a yanked version (yanked);tarball-mismatch), with nothing extracted;--allow-single-factor;-cwith the release configs;--fromwith no fetch, and--fromwithout a validyanked.json;--discard-local-themelisting the files not deployed;comments.available: false,media.transform: cdn-cgi, an unknown key);release-theme-check.test.ts(7). These are the RFC's list:src/, addedpublic/, a deleted starter file, an editedastro.config.mjsand an addedskills/x/SKILL.mdare each named;tsconfig.json,jsconfig.json,postcss.config.mjs,.postcssrc.jsonand apostcsskey are each named;postcss.config.cjsand apostcsskey in between are found;.DS_Store,Thumbs.db,desktop.ini,vite.config.ts,.env,README.md,.gitignoreandwrangler.jsoncpass;config-build-keys.test.ts(6), the key-table guards:init's config, and deploy and runtime keys, pass;media.transform;OWN_KEYSis classed;project.<key>in the starter'sastro.config.mjsis classed;tsc);prepareAdminWorkerwith different mail, ai, browser, observability, placement, domain and siteId writes a byte-identical entry, skills and design module; only the wrangler config differs.release-extract.test.ts(2): a fresh directory each time,_headerswritten, and../and duplicate entries refused.Existing suites, all passing:
Checks: cli tsc, biome on the changed files, and size:check are clean, and the docs suite passes (41).
Not in this PR
Two drift guards that §11b lists outside the CLI:
CLOUDHAL_SITE_IDthat differs from the baked id;cloudflaretarget ignoresdataDir.The first is now added (review round 1); the second is #239.
Refs #227 (its done-when needs
@cloudhal/release-verifypublished; follow-up #239)🤖 Generated with Claude Code